import type { Options } from 'prettier'
import { format } from 'prettier/standalone'
import typescript from 'prettier/plugins/typescript'
import estree from 'prettier/plugins/estree'
import type { ApiConfig, ApiDocs, ObjectSchema, Schema, Source } from './types'
import { isObject, upperFirst } from '@shared/utils'

const prettierConfig: Options = {
  plugins: [typescript, estree],
  parser: 'typescript',
  printWidth: 160,
  trailingComma: 'es5',
  bracketSpacing: true,
  singleQuote: true,
  semi: true,
  arrowParens: 'avoid',
  endOfLine: 'lf',
  useTabs: false,
  tabWidth: 2,
  vueIndentScriptAndStyle: false,
  singleAttributePerLine: true
}

function normalizeSource({ paths, components }: ApiDocs): Source {
  const operationIdMap: { [k: string]: ApiConfig } = {}
  const pathLinks = new Set<string>()

  Object.entries(paths).forEach(([path, methods]) => {
    Object.entries(methods).forEach(([method, config]) => {
      pathLinks.add(path)
      Object.assign(config, { path, method })
      operationIdMap[config.operationId] = config
    })
  })

  return { operationIdMap, paths: [...pathLinks], components: components.schemas }
}

const trans = {
  ',': '，',
  '\\.': '。',
  '\\(': '（',
  '\\)': '）',
  ':': '：',
  integer: 'number'
}

const map = Object.entries(trans).reduce<Record<string, string>>((obj, [k, v]) => {
  obj[k.charAt(k.length - 1)] = v
  return obj
}, {})

const regexp = new RegExp(`(${Object.keys(trans).join('|')})`, 'g')

function replace(text: string) {
  return text?.replace(regexp, (char) => map[char] ?? char)
}

enum ComponentType {
  RESPONSE,
  REQUEST,
  PARAMETER
}

export class SwaggerToInterface {
  #source: Source

  #currentComponentId = new Map<string, ComponentType>()

  constructor(source: ApiDocs) {
    this.#source = normalizeSource(source)
  }

  get operationId() {
    if (this.#source?.operationIdMap) {
      return Object.keys(this.#source.operationIdMap)
    }
    return []
  }

  get componentId() {
    if (this.#source?.components) {
      return Object.keys(this.#source.components)
    }
    return []
  }

  get paths() {
    return this.#source?.paths ?? []
  }

  getInterface(option: { operationId: string } | { path: string; method: string }) {
    const config = this.#findApiConfig(option)
    if (config) {
      const source = this.#transformApiConfig(config)
      if (source) {
        return format(source, prettierConfig).catch(() => source)
      }
    }
    return Promise.reject('当前链接无法解析为接口')
  }

  getPath(option: { operationId: string }) {
    return this.#findApiConfig(option)?.path
  }

  getTitle(option: { operationId: string }) {
    const config = this.#findApiConfig(option)
    return config ? this.#createTitle(config) : ''
  }

  #createTitle(config: ApiConfig, subtitle?: string) {
    const { tags, summary } = config
    let title = ''
    if (tags?.length && summary) {
      title = `${tags.join('/')} - ${summary}`
    } else if (summary) {
      title = summary
    } else if (tags?.length) {
      title = tags.join('/')
    }
    if (title && subtitle) {
      return `${title} - ${subtitle}`
    }
    if (!title && subtitle) {
      return subtitle
    }
    return title
  }

  #findApiConfig(
    option: { operationId: string } | { path: string; method: string }
  ): ApiConfig | void {
    if (this.#source) {
      const { operationIdMap } = this.#source
      if ('operationId' in option) {
        return operationIdMap[option.operationId]
      } else {
        for (const key in operationIdMap) {
          const item = operationIdMap[key]
          if (item.path === option.path && item.method === option.method) return item
        }
      }
    }
  }

  #transformApiConfig(config: ApiConfig) {
    this.#currentComponentId.clear()

    const stack: string[] = []
    const { operationId, parameters, requestBody, responses } = config

    const interfaceBase = upperFirst(operationId)

    // 返回体数据结构
    for (const code in responses) {
      if (responses[code].content) {
        const annotation = createAnnotation(this.#createTitle(config, '接口返回类型'))
        const { schema } = Object.values(responses[code].content!)[0]
        stack.push(
          `${annotation}\n export type ${interfaceBase}Response = ${this.#transformType(
            schema,
            ComponentType.RESPONSE
          )};`
        )
      }
    }

    // 请求体数据结构
    if (requestBody) {
      const annotation = createAnnotation(this.#createTitle(config, '接口请求体类型'))
      const { schema } = Object.values(requestBody.content!)[0]
      stack.push(
        `${annotation}\n export type ${interfaceBase}Request = ${this.#transformType(
          schema,
          ComponentType.REQUEST
        )};`
      )
    }

    if (parameters?.length) {
      const body = parameters.map(({ name, description, required, schema, in: type }) => {
        return `/** ${replace(description)} */\n ${name}${
          required ? '' : '?'
        }: ${this.#transformType(schema, ComponentType.PARAMETER)};`
      })
      const annotation = createAnnotation(this.#createTitle(config, '接口请求参数类型'))
      stack.push(
        `${annotation}\n export type ${interfaceBase}Parameter = {\n${body.join(`\n`)}\n};`
      )
    }

    const { components } = this.#source!

    this.#currentComponentId.forEach((type, componentId) => {
      stack.push(this.#transformObjectSchema(components[componentId], type))
    })

    return stack.reverse().join('\n\n')
  }

  #transformObjectSchema(schema: ObjectSchema, type: ComponentType) {
    const { title } = schema
    const { properties, description, required = [] } = schema

    const stack: string[] = []
    description && stack.push(createAnnotation(replace(description)))
    stack.push(`export interface ${title} {`)

    for (const key in properties) {
      const propertySchema = properties[key]
      if ('description' in propertySchema) {
        stack.push(`/** ${replace(propertySchema.description!)} */`)
      }
      stack.push(
        `${key}${
          type === ComponentType.RESPONSE || required.includes(key) ? '' : '?'
        }:${this.#transformType(propertySchema, type)};`
      )
    }

    stack.push('}')

    return stack.join('\n')
  }

  #transformType(schema: Schema, type: ComponentType): string {
    if (!isObject(schema)) return 'unknown'
    if ('$ref' in schema) {
      const componentId = schema.$ref.split('/').at(-1)

      if (componentId && this.#source && componentId in this.#source.components) {
        this.#currentComponentId.set(componentId, type)
        return componentId
      }

      return 'unknown'
    }

    switch (schema.type) {
      case 'integer':
      case 'int32':
      case 'int64':
        return 'number'

      case 'number':
      case 'string':
      case 'boolean':
        return schema.type

      case 'object':
        return schema.title

      case 'array':
        return `${this.#transformType(schema.items, type)}[]`

      default:
        return 'unknown'
    }
  }
}

function createAnnotation(...lines: string[]) {
  const stack: string[] = [`/**`]
  lines.forEach((line) => {
    stack.push(` * ${line}`)
  })
  stack.push(`*/`)

  return stack.join(`\n`)
}
